package hypeerweb;

import java.awt.event.ActionListener;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Timer;

import communications.GlobalObjectId;
import communications.LocalObjectId;
import communications.PortNumber;

/**
 * Handles important database access details in our hypeerweb<br />
 *<br />
 * Table Schema as defined by our DB Model					*<br />
 *==================================================		*<br />
 * Table Name: Nodes										*<br />
 *==================================================		*<br />
 * NodeId	| State 	| FoldId	| SFoldId	| ISFoldId	*<br />
 * -------------------------------------------------		*<br />
 * PK-int	| int		| int		| int		| int		*<br />
 *==================================================		*<br />
 *<br />
 *======================	*<br />
 * Table Name: Neighbors	*<br />
 *======================	*<br />
 * NodeId	| NeigbhorID	*<br />
 *----------------------	*<br />
 * FK-int	| FK-int		*<br />
 *======================	*<br />
 *
 *======================	*<br />
 * TableName: SNeighbors	*<br />
 *======================	*<br />
 * NodeId	| SNeigbhorID	*<br />
 *----------------------	*<br />
 * FK-int	| FK-int		*<br />
 *======================	*<br />
 */
public final class HyPeerWebDatabase {
    private static final Logger log = Logger.getLogger(HyPeerWebDatabase.class.getName());

    /*Default directory and database name*/
	public static final java.lang.String DEFAULT_DATABASE_NAME = "HyPeerWeb.db";

	// Table Names
	public static final String NODES = "Nodes";
	public static final String NODE_PROXIES = "NodeProxies";
	public static final String NEIGHBORS = "Neighbors";
	public static final String S_NEIGHBORS = "SurrogateNeighbors";
	public static final String I_NEIGHBORS = "InvSurrogateNeighbors";

	// Field Names
	public static final String NODE_ID = "NodeId";
	public static final String FOLD_ID = "FoldId";
	public static final String S_FOLD_ID = "SFoldId";
	public static final String I_S_FOLD_ID = "ISFoldId";
	public static final String NEIGHBOR_ID = "NeighborId";
	public static final String S_NEIGHBOR_ID = "SNeigbhorId";
	public static final String I_NEIGHBOR_ID = "INeigbhorId";
	public static final String STATE = "State";

	public static final String MACHINE_ADDRESS = "MachineAddress";
	public static final String PORT_NUMBER = "PortNumber";
	public static final String LOCAL_OBJECT_ID = "LocalObjectId";

	// this value is used in the database to indicate no node association
	private static final int NO_NODE = -1;

	//The singleton
	protected static HyPeerWebDatabase instance = null;

	//The Database connection
    private static Connection db = null;
    
    // in milliseconds
	public static final int COMMIT_DELAY = 100;

    public static HyPeerWebDatabase getSingleton() {
		//log.info("getting Singleton");
		if(instance == null) {
			instance = new HyPeerWebDatabase();
		}
		return instance;
    }

    private HyPeerWebDatabase() {
        initHyPeerWebDatabase(DEFAULT_DATABASE_NAME);
    }

    /**
    * Initialize the database using the given database name <br>
    * <br>
    * @pre <i>None</i>
    * @post db is a valid connection to the database with the given name
    * @param dbName the name of the database to use for storing the information
    */
    static public void initHyPeerWebDatabase(java.lang.String dbName) {
		try {
			if (dbName == null || dbName.length() == 0) {
				dbName = DEFAULT_DATABASE_NAME;
			}

			//Make sure to close any connection we already have
			if(db != null)
			{
				db.close();
				db = null;
			}

			Class.forName("org.sqlite.JDBC");
			db = DriverManager.getConnection("jdbc:sqlite:"+dbName);
			Statement stat = db.createStatement();

			String createNodesTable = "create table if not exists " + NODES +
					" ("+
					NODE_ID+", " +
					STATE +", "+
					FOLD_ID+", "+
					S_FOLD_ID+", "+
					I_S_FOLD_ID+", "+
					LOCAL_OBJECT_ID+", "+
					"PRIMARY KEY("+
					NODE_ID+"));";
			// End temp code

			String createNeighborTable = "create table if not exists "+NEIGHBORS+
					" ("+
					NODE_ID+", "+
					NEIGHBOR_ID + ", " +
					"primary key(" +
						NODE_ID + ", " +
						NEIGHBOR_ID +
					"));";

			String createSNeighborTable = "create table if not exists "+S_NEIGHBORS+
					" ("+
					NODE_ID+", "+
					S_NEIGHBOR_ID + ", " +
					"primary key("+
						NODE_ID + ", " +
						S_NEIGHBOR_ID +
					"));";

			String createINeighborTable = "create table if not exists "+I_NEIGHBORS+
					" ("+
					NODE_ID+", "+
					I_NEIGHBOR_ID + ", " +
					"primary key("+
						NODE_ID + ", " +
						I_NEIGHBOR_ID +
					"));";


			String createProxyNodeTable = "create table if not exists "+NODE_PROXIES+
					" ("+
					NODE_ID+", "+
					MACHINE_ADDRESS + ", " +
					PORT_NUMBER + ", " +
					LOCAL_OBJECT_ID + ", " +
					"primary key("+
						NODE_ID +
					"));";

			//create tables
			stat.executeUpdate(createNodesTable);
			stat.executeUpdate(createNeighborTable);
			stat.executeUpdate(createSNeighborTable);
			stat.executeUpdate(createINeighborTable);
			stat.executeUpdate(createProxyNodeTable);

		} catch (ClassNotFoundException ex) {
			Logger.getLogger(HyPeerWebDatabase.class.getName()).log(Level.SEVERE, null, ex);
		} catch (SQLException ex) {
			ex.printStackTrace();
			System.exit(-1);
		}
    }
    
    private boolean isNodeSaved(int webId, String table) {
    	try
    	{
			Statement selectStatement = db.createStatement();
			ResultSet countResult = selectStatement.executeQuery("select * from " + table + " where " + NODE_ID + "=" + webId);
			
			if (countResult.next()) {
				return true;
			}
			else {
				return false;
			}
    	}
    	catch (SQLException e)
    	{
    		System.err.println(e.getMessage());
    		e.printStackTrace();
    		
    		return false;
    	}
    }
    
    public boolean isNodeSaved(int webId) {
    	return isNodeSaved(webId, NODES);
    }
    
    public boolean isProxyNodeSaved(int webId) {
    	return isNodeSaved(webId, NODE_PROXIES);
    }

	/**
	 * Creates a SimplifiedNodeDomain representing the node with indicated webId.
	 * The information is retrieved from the database.
	 *
	 * Parameters:
	 *	  webId - The webId of the node whose information we are going to retrieve.
	 * Pre-condition:
	 *	  There exists a node in the database with the given webId.
	 * Post-condition:
	 *	  result contains the webId, neighbors, upPointers, downPointers,
	 *    fold, surrogateFold, and inverse surrogate fold of the indicated node.
	 *
	 * @param webId
	 * @return SimplifiedNodeDomain
	 */
    public SimplifiedNodeDomain getNode(int webId) {
        ArrayList<hypeerweb.Node> result = loadNodes();
        
        if (webId < result.size()){
            return result.get(webId).constructSimplifiedNodeDomain();
        } else {
            return hypeerweb.Node.NULL_NODE.constructSimplifiedNodeDomain();
        }
    }

   /**
    * removes all information from the database
	* @pre db is a valid connection
	* @post All information stored in the database is removed
    */
   public void clear() {
	   try {
            Statement stat = db.createStatement();
            stat.execute("delete from " + NODES);
            stat.execute("delete from " + NEIGHBORS);
            stat.execute("delete from " + S_NEIGHBORS);
            stat.execute("delete from " + I_NEIGHBORS);
            stat.execute("delete from " + NODE_PROXIES);
            
	   } catch (SQLException ex) {
            Logger.getLogger(HyPeerWebDatabase.class.getName()).log(Level.SEVERE, null, ex);
	   }
   }

   /**
   * saves all of the nodes in the array to the database
   * @param nodes the nodes to be saved to the database
   * @pre db is a valid connection
   * @post all nodes in the array are saved in the database
   */
   public void saveNodes(Collection<hypeerweb.Node> nodes) {
	   clear();
	   SaveNodeVisitor saveVisitor = new SaveNodeVisitor(this, db);

	   for (hypeerweb.Node node : nodes) {
		   node.accept(saveVisitor, null);
	   }
   }

   /**
    * Gets the number of nodes saved in the database
	* @pre db is a valid connection
    * @return the number of nodes saved in the database
    */
   private int getSavedNodeCount() throws SQLException {
	   Statement selectStatement = db.createStatement();
	   ResultSet countResult = selectStatement.executeQuery("select count(*) from " + NODES);

	   int result = 0;

	   if (countResult.next()) {
		   result = countResult.getInt(1);
	   }
	   else {
		   result = 0;
	   }
	   countResult.close();
	   return result;
   }

   /**
    *
    * @author James Lambert
    *
    * used as a data storage class for loading information from the database
    */
   private class NodeTuple {
	   private int nodeID;
	   private int nodeState;
	   private int foldID;
	   private int surragoteFoldID;
	   private int invSurragoteFoldID;
	   private int localObjectId;

	   public NodeTuple(ResultSet row) throws SQLException {
		   nodeID = row.getInt(NODE_ID);
		   nodeState = row.getInt(STATE);
		   foldID = row.getInt(FOLD_ID);
		   surragoteFoldID = row.getInt(S_FOLD_ID);
		   invSurragoteFoldID = row.getInt(I_S_FOLD_ID);
		   localObjectId = row.getInt(LOCAL_OBJECT_ID);
	   }

	   public int getNodeID() {
		   return nodeID;
	   }

	   public NodeState getNodeState() {
		   return NodeState.getNodeState(nodeState);
	   }

	   public int getFoldID() {
		   return foldID;
	   }

	   public int getSurragoteFoldID() {
		   return surragoteFoldID;
	   }

	   public int getInvSurragoteFoldID() {
		   return invSurragoteFoldID;
	   }
	   
	   public LocalObjectId getLocalObjectId() {
		   return new LocalObjectId(localObjectId);
	   }
   }

   /**
    * Loads the node information from the database
    * @return an array of node tuples sorted ascending by NODE_ID
    */
   private ArrayList<NodeTuple> loadNodeTuples() throws SQLException {
	   Statement selectStatement = db.createStatement();
	   ResultSet selectResult = selectStatement.executeQuery("select * from " + NODES + " order by " + NODE_ID + " ASC");

	   ArrayList<NodeTuple> result = new ArrayList<NodeTuple>(getSavedNodeCount());

	   while (selectResult.next()) {
		   result.add(new NodeTuple(selectResult));
	   }
	   return result;
   }

   private List<NodeProxy> loadProxyNodes() throws SQLException {
	   Statement selectStatement = db.createStatement();
	   ResultSet selectResult = selectStatement.executeQuery("select * from " + NODE_PROXIES + " order by " + NODE_ID + " ASC");

	   ArrayList<NodeProxy> result = new ArrayList<NodeProxy>();

	   while (selectResult.next()) {
		   int webid = selectResult.getInt(NODE_ID);
		   String machineAddress = selectResult.getString(MACHINE_ADDRESS);
		   int portNumber = selectResult.getInt(PORT_NUMBER);
		   int localId = selectResult.getInt(LOCAL_OBJECT_ID);
		   
		   LocalObjectId localObjectId = new LocalObjectId(localId);
		   GlobalObjectId globalId = new GlobalObjectId(machineAddress, new PortNumber(portNumber), localObjectId);
		   result.add(new NodeProxy(globalId, webid));
	   }
	   return result;
   }

   /**
    * Connects the neighbors between the nodes
    * @param nodes a list of nodes sorted by webid.
    */
   private void linkUpNeighbors(Map<Integer, Node> allNodes) throws SQLException {
	   Statement selectStatement = db.createStatement();
	   ResultSet selectResult = selectStatement.executeQuery("select * from " + NEIGHBORS);

	   while (selectResult.next()) {
		   int nodeIDA = selectResult.getInt(NODE_ID);
		   int nodeIDB = selectResult.getInt(NEIGHBOR_ID);

		   allNodes.get(nodeIDA).addNeighbor(allNodes.get(nodeIDB));
	   }
	   selectResult.close();
   }

   /**
    * Connects the surrogate neighbors between the nodes
    * @param nodes
    */
   private void linkUpSurrogateNeighbors(Map<Integer, Node> allNodes) throws SQLException {
	   Statement selectStatement = db.createStatement();
	   ResultSet selectResult = selectStatement.executeQuery("select * from " + S_NEIGHBORS);

	   while (selectResult.next()) {
		   int nodeIDA = selectResult.getInt(NODE_ID);
		   int surrogateIDB = selectResult.getInt(S_NEIGHBOR_ID);

		   allNodes.get(nodeIDA).addDownPointer(allNodes.get(surrogateIDB));
	   }
	   selectResult.close();
   }

   /**
    * Connects the surrogate neighbors between the nodes
    * @param nodes
    */
   private void linkUpInverseSurrogateNeighbors(Map<Integer, Node> allNodes) throws SQLException {
	   Statement selectStatement = db.createStatement();
	   ResultSet selectResult = selectStatement.executeQuery("select * from " + I_NEIGHBORS);

	   while (selectResult.next()) {
		   int nodeIDA = selectResult.getInt(NODE_ID);
		   int surrogateIDB = selectResult.getInt(I_NEIGHBOR_ID);

		   allNodes.get(nodeIDA).addUpPointer(allNodes.get(surrogateIDB));
	   }
	   selectResult.close();
   }

   /**
    * Loads all of the nodes from the database
    * @return an array of nodes with their neighbors, fold, and surragotes already connected
    */
   public ArrayList<hypeerweb.Node> loadNodes() {
//	   commitChanges();
	   
	   try {
		   ArrayList<NodeTuple> nodeTuples = loadNodeTuples();
		   ArrayList<hypeerweb.Node> result = new ArrayList<hypeerweb.Node>(nodeTuples.size());
		   Map<Integer, hypeerweb.Node> allNodes = new HashMap<Integer, hypeerweb.Node>(nodeTuples.size());

		   // initialize result node list
		   for (int i = 0; i < nodeTuples.size(); ++i) {
			   NodeTuple currentNodeTuple = nodeTuples.get(i);
//			   assert i == currentNodeTuple.getNodeID();
			   Node newNode = new hypeerweb.Node(currentNodeTuple.getNodeID(), 0);
                           newNode.setState(currentNodeTuple.getNodeState());
			   result.add(newNode);
			   allNodes.put(newNode.getWebIdValue(), newNode);
		   }

		   List<NodeProxy> proxyNodes = loadProxyNodes();
		   
		   for (NodeProxy proxy : proxyNodes)
		   {
			   allNodes.put(proxy.getWebIdValue(), proxy);
		   }

		   // link up folds and surragote folds
		   for (int i = 0; i < nodeTuples.size(); ++i) {
			   hypeerweb.Node currentNode = result.get(i);
			   NodeTuple currentNodeTuple = nodeTuples.get(i);
			   currentNode.setLocalObjectId(currentNodeTuple.getLocalObjectId());

			   if (currentNodeTuple.getFoldID() != NO_NODE) {
				   currentNode.setFold(allNodes.get(currentNodeTuple.getFoldID()));
			   } else {
				   currentNode.setFold(hypeerweb.Node.NULL_NODE);
			   }

			   if (currentNodeTuple.getSurragoteFoldID() != NO_NODE) {
				   currentNode.setSurrogateFold(allNodes.get(currentNodeTuple.getSurragoteFoldID()));
			   } else {
				   currentNode.setSurrogateFold(hypeerweb.Node.NULL_NODE);
			   }

			   if (currentNodeTuple.getInvSurragoteFoldID() != NO_NODE) {
				   currentNode.setInverseSurrogateFold(allNodes.get(currentNodeTuple.getInvSurragoteFoldID()));
			   } else {
				   currentNode.setInverseSurrogateFold(hypeerweb.Node.NULL_NODE);
			   }
		   }

		   linkUpNeighbors(allNodes);
		   linkUpSurrogateNeighbors(allNodes);
		   linkUpInverseSurrogateNeighbors(allNodes);

		   return result;
	   } catch (SQLException ex) {
		   Logger.getLogger(HyPeerWebDatabase.class.getName()).log(Level.SEVERE, null, ex);
		   return null;
	   }
   }

}
